﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine.Localization.SmartFormat.Core.Extensions;
using UnityEngine.Localization.SmartFormat.Utilities;
using UnityEngine.Pool;

namespace Runtime {

    public sealed class DFR_TimeFormatter : FormatterBase {

        public const string ZH_HANS = "zh-Hans";
        public const string ZH_HANT = "zh-Hant";

        internal const TimeSpanFormatOptions AbbreviateAll = TimeSpanFormatOptions.Abbreviate | TimeSpanFormatOptions.AbbreviateOff;
        internal const TimeSpanFormatOptions LessThanAll = TimeSpanFormatOptions.LessThan | TimeSpanFormatOptions.LessThanOff;
        internal const TimeSpanFormatOptions RangeAll = TimeSpanFormatOptions.RangeMilliSeconds | TimeSpanFormatOptions.RangeSeconds | TimeSpanFormatOptions.RangeMinutes | TimeSpanFormatOptions.RangeHours | TimeSpanFormatOptions.RangeDays | TimeSpanFormatOptions.RangeWeeks;
        internal const TimeSpanFormatOptions TruncateAll = TimeSpanFormatOptions.TruncateShortest | TimeSpanFormatOptions.TruncateAuto | TimeSpanFormatOptions.TruncateFill | TimeSpanFormatOptions.TruncateFull;

        static class SystemTime {
            public static Func<DateTime> Now = () => DateTime.Now;
            public static void SetDateTime(DateTime dateTimeNow) {
                Now = () => dateTimeNow;
            }
            public static Func<DateTimeOffset> OffsetNow = () => DateTimeOffset.Now;
            public static void SetDateTimeOffset(DateTimeOffset dateTimeOffset) {
                OffsetNow = () => dateTimeOffset;
            }
            public static void ResetDateTime() {
                Now = () => DateTime.Now;
                OffsetNow = () => DateTimeOffset.Now;
            }
        }

        public static TimeTextInfo ChineseSimplified => new(
            PluralRules.GetPluralRule("zh"),
            new[] { "{0}周" },
            new[] { "{0}天" },
            new[] { "{0}小时" },
            new[] { "{0}分钟" },
            new[] { "{0}秒" },
            new[] { "{0}毫秒" },
            new[] { "{0}周" },
            new[] { "{0}天" },
            new[] { "{0}时" },
            new[] { "{0}分" },
            new[] { "{0}秒" },
            new[] { "{0}毫秒" },
            "小于{0}"
        );
        public static TimeTextInfo ChineseTraditional => new(
            PluralRules.GetPluralRule("zh"),
            new[] { "{0}週" },
            new[] { "{0}天" },
            new[] { "{0}小時" },
            new[] { "{0}分鐘" },
            new[] { "{0}秒" },
            new[] { "{0}毫秒" },
            new[] { "{0}週" },
            new[] { "{0}天" },
            new[] { "{0}時" },
            new[] { "{0}分" },
            new[] { "{0}秒" },
            new[] { "{0}毫秒" },
            "小於{0}"
        );

        public static TimeTextInfo GetTimeTextInfo(string isoLanguageName) {
            var result = CommonLanguagesTimeTextInfo.GetTimeTextInfo(isoLanguageName);
            result ??= isoLanguageName switch {
                "zh-Hans" => ChineseSimplified,
                "zh-Hant" => ChineseTraditional,
                _ => throw new NotSupportedException(),
            };
            return result;
        }

        public override string[] DefaultNames => new string[] { "dks_timespan", "dks_time", "dks_t", "" };

        public override bool TryEvaluateFormat(IFormattingInfo formattingInfo) {
            var format = formattingInfo.Format;
            var current = formattingInfo.CurrentValue;

            if (format != null && format.HasNested)
                return false;
            string options;
            if (!string.IsNullOrEmpty(formattingInfo.FormatterOptions))
                options = formattingInfo.FormatterOptions;
            else if (format != null)
                options = format.GetLiteralText();
            else
                options = string.Empty;

            TimeSpan fromTime;

            switch (current) {
                case TimeSpan timeSpan:
                    fromTime = timeSpan;
                    break;
                case DateTime dateTime:
                    if (formattingInfo.FormatterOptions != string.Empty) {
                        fromTime = SystemTime.Now().ToUniversalTime().Subtract(dateTime.ToUniversalTime());
                    } else {
                        return false;
                    }
                    break;
                case DateTimeOffset dateTimeOffset:
                    if (formattingInfo.FormatterOptions != string.Empty) {
                        fromTime = SystemTime.OffsetNow().UtcDateTime.Subtract(dateTimeOffset.UtcDateTime);
                    } else {
                        return false;
                    }
                    break;
                default:
                    return false;
            }
            var isoLanguageName = formattingInfo.FormatDetails.Provider.ToString();
            var timeTextInfo = GetTimeTextInfo(isoLanguageName);
            if (timeTextInfo == null)
                return false;
            var formattingOptions = TimeSpanFormatOptionsConverter.Parse(options);
            var timeString = ToTimeString(isoLanguageName, fromTime, formattingOptions, timeTextInfo);
            formattingInfo.Write(timeString);
            return true;
        }

        public static string ToTimeString(string isoLanguageName, TimeSpan FromTime, TimeSpanFormatOptions options,
                TimeTextInfo timeTextInfo) {
            // If there are any missing options, merge with the defaults:
            // Also, as a safeguard against missing DefaultFormatOptions, let's also merge with the AbsoluteDefaults:
            options = options.Merge(TimeSpanUtility.DefaultFormatOptions).Merge(TimeSpanUtility.AbsoluteDefaults);
            // Extract the individual options:
            var rangeMax = options.Mask(RangeAll).AllFlags().Last();
            var rangeMin = options.Mask(RangeAll).AllFlags().First();
            var truncate = options.Mask(TruncateAll).AllFlags().First();
            var lessThan = options.Mask(LessThanAll) != TimeSpanFormatOptions.LessThanOff;
            var abbreviate = options.Mask(AbbreviateAll) != TimeSpanFormatOptions.AbbreviateOff;

            var round = lessThan ? (Func<double, double>)Math.Floor : Math.Ceiling;
            switch (rangeMin) {
                case TimeSpanFormatOptions.RangeWeeks:
                    FromTime = TimeSpan.FromDays(round(FromTime.TotalDays / 7) * 7);
                    break;
                case TimeSpanFormatOptions.RangeDays:
                    FromTime = TimeSpan.FromDays(round(FromTime.TotalDays));
                    break;
                case TimeSpanFormatOptions.RangeHours:
                    FromTime = TimeSpan.FromHours(round(FromTime.TotalHours));
                    break;
                case TimeSpanFormatOptions.RangeMinutes:
                    FromTime = TimeSpan.FromMinutes(round(FromTime.TotalMinutes));
                    break;
                case TimeSpanFormatOptions.RangeSeconds:
                    FromTime = TimeSpan.FromSeconds(round(FromTime.TotalSeconds));
                    break;
                case TimeSpanFormatOptions.RangeMilliSeconds:
                    FromTime = TimeSpan.FromMilliseconds(round(FromTime.TotalMilliseconds));
                    break;
            }

            // Create our result:
            var textStarted = false;
            var result = StringBuilderPool.Get();
            for (var i = rangeMax; i >= rangeMin; i = (TimeSpanFormatOptions)((int)i >> 1)) {
                // Determine the value and title:
                int value;
                switch (i) {
                    case TimeSpanFormatOptions.RangeWeeks:
                        value = (int)Math.Floor(FromTime.TotalDays / 7);
                        FromTime -= TimeSpan.FromDays(value * 7);
                        break;
                    case TimeSpanFormatOptions.RangeDays:
                        value = (int)Math.Floor(FromTime.TotalDays);
                        FromTime -= TimeSpan.FromDays(value);
                        break;
                    case TimeSpanFormatOptions.RangeHours:
                        value = (int)Math.Floor(FromTime.TotalHours);
                        FromTime -= TimeSpan.FromHours(value);
                        break;
                    case TimeSpanFormatOptions.RangeMinutes:
                        value = (int)Math.Floor(FromTime.TotalMinutes);
                        FromTime -= TimeSpan.FromMinutes(value);
                        break;
                    case TimeSpanFormatOptions.RangeSeconds:
                        value = (int)Math.Floor(FromTime.TotalSeconds);
                        FromTime -= TimeSpan.FromSeconds(value);
                        break;
                    case TimeSpanFormatOptions.RangeMilliSeconds:
                        value = (int)Math.Floor(FromTime.TotalMilliseconds);
                        FromTime -= TimeSpan.FromMilliseconds(value);
                        break;
                    default:
                        // This code is unreachable, but it prevents compile-errors.
                        throw new ArgumentException("TimeSpanUtility");
                }


                //Determine whether to display this value
                var displayThisValue = false;
                var breakFor =
                    false; // I wish C# supported "break for;" (like how VB supports "Exit For" from within a "Select Case" statement)
                switch (truncate) {
                    case TimeSpanFormatOptions.TruncateShortest:
                        if (textStarted) {
                            breakFor = true;
                            break;
                        }

                        if (value > 0)
                            displayThisValue = true;
                        break;
                    case TimeSpanFormatOptions.TruncateAuto:
                        if (value > 0)
                            displayThisValue = true;
                        break;
                    case TimeSpanFormatOptions.TruncateFill:
                        if (textStarted || value > 0)
                            displayThisValue = true;
                        break;
                    case TimeSpanFormatOptions.TruncateFull:
                        displayThisValue = true;
                        break;
                }

                if (breakFor)
                    break;

                //we need to display SOMETHING (even if it's zero)
                if (i == rangeMin && textStarted == false) {
                    displayThisValue = true;
                    if (lessThan && value < 1) {
                        // Output the "less than 1 unit" text:
                        var unitTitle = timeTextInfo.GetUnitText(rangeMin, 1, abbreviate);
                        result.Append(timeTextInfo.GetLessThanText(unitTitle));
                        displayThisValue = false;
                    }
                }

                // Output the value:
                if (displayThisValue) {
                    if (textStarted) {
                        switch (isoLanguageName) {
                            case "en":
                                result.Append(" ");
                                break;
                            case "zh-Hans":
                            case "zh-Hant":
                                break;
                            default:
                                throw new NotSupportedException();
                        }
                    }
                    var unitTitle = timeTextInfo.GetUnitText(i, value, abbreviate);
                    result.Append(unitTitle);
                    textStarted = true;
                }
            }

            var ret = result.ToString();
            StringBuilderPool.Release(result);
            return ret;
        }

        static class StringBuilderPool {

            internal static readonly ObjectPool<StringBuilder> s_Pool = new(() => new(), null, sb => sb.Clear(), collectionCheck: false);

            public static StringBuilder Get() => s_Pool.Get();
            public static PooledObject<StringBuilder> Get(out StringBuilder value) => s_Pool.Get(out value);
            public static void Release(StringBuilder toRelease) => s_Pool.Release(toRelease);
        }
    }

    internal static class TimeSpanFormatOptionsConverter {

        private static readonly Regex parser =
            new(
                @"\b(w|week|weeks|d|day|days|h|hour|hours|m|minute|minutes|s|second|seconds|ms|millisecond|milliseconds|auto|short|fill|full|abbr|noabbr|less|noless)\b",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

        public static TimeSpanFormatOptions Merge(this TimeSpanFormatOptions left, TimeSpanFormatOptions right) {
            var masks = new[]
            {
                DFR_TimeFormatter.AbbreviateAll,
                DFR_TimeFormatter.LessThanAll,
                DFR_TimeFormatter.RangeAll,
                DFR_TimeFormatter.TruncateAll
            };
            foreach (var mask in masks)
                if ((left & mask) == 0)
                    left |= right & mask;
            return left;
        }

        public static TimeSpanFormatOptions Mask(this TimeSpanFormatOptions timeSpanFormatOptions,
            TimeSpanFormatOptions mask) {
            return timeSpanFormatOptions & mask;
        }

        public static IEnumerable<TimeSpanFormatOptions> AllFlags(this TimeSpanFormatOptions timeSpanFormatOptions) {
            uint value = 0x1;
            while (value <= (uint)timeSpanFormatOptions) {
                if ((value & (uint)timeSpanFormatOptions) != 0)
                    yield return (TimeSpanFormatOptions)value;
                value <<= 1;
            }
        }

        public static TimeSpanFormatOptions Parse(string formatOptionsString) {
            formatOptionsString = formatOptionsString.ToLower();

            var t = TimeSpanFormatOptions.InheritDefaults;
            foreach (Match m in parser.Matches(formatOptionsString))
                switch (m.Value) {
                    case "w":
                    case "week":
                    case "weeks":
                        t |= TimeSpanFormatOptions.RangeWeeks;
                        break;
                    case "d":
                    case "day":
                    case "days":
                        t |= TimeSpanFormatOptions.RangeDays;
                        break;
                    case "h":
                    case "hour":
                    case "hours":
                        t |= TimeSpanFormatOptions.RangeHours;
                        break;
                    case "m":
                    case "minute":
                    case "minutes":
                        t |= TimeSpanFormatOptions.RangeMinutes;
                        break;
                    case "s":
                    case "second":
                    case "seconds":
                        t |= TimeSpanFormatOptions.RangeSeconds;
                        break;
                    case "ms":
                    case "millisecond":
                    case "milliseconds":
                        t |= TimeSpanFormatOptions.RangeMilliSeconds;
                        break;


                    case "short":
                        t |= TimeSpanFormatOptions.TruncateShortest;
                        break;
                    case "auto":
                        t |= TimeSpanFormatOptions.TruncateAuto;
                        break;
                    case "fill":
                        t |= TimeSpanFormatOptions.TruncateFill;
                        break;
                    case "full":
                        t |= TimeSpanFormatOptions.TruncateFull;
                        break;


                    case "abbr":
                        t |= TimeSpanFormatOptions.Abbreviate;
                        break;
                    case "noabbr":
                        t |= TimeSpanFormatOptions.AbbreviateOff;
                        break;


                    case "less":
                        t |= TimeSpanFormatOptions.LessThan;
                        break;
                    case "noless":
                        t |= TimeSpanFormatOptions.LessThanOff;
                        break;
                }

            return t;
        }
    }
}
